package com.wgo.bpot.wiring.ejb;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import com.wgo.bpot.common.transport.exception.RematoException;
import com.wgo.bpot.common.transport.exception.db.DatabaseException;
import com.wgo.bpot.common.transport.util.MethodNamingConvention;
import com.wgo.bpot.common.transport.util.ReflectionHelper;
import com.wgo.bpot.domain.common.User;
import com.wgo.bpot.domain.server.ejb.UserEjb;
import com.wgo.bpot.server.persist.UniqueValue;
import com.wgo.bpot.server.persist.hibernate.UniqueValueEjb;
import com.wgo.bpot.wiring.common.DomainModelConvention;
import com.wgo.bpot.wiring.common.DomainModelConventionImpl;

/**
 * @author PEide3
 * @TODO: add tests
 */
public abstract class DomainConventionEjbImpl<T> extends DomainModelConventionImpl<T> implements EjbModelConventionImpl<T> {

	private static final transient Logger log = Logger.getLogger(DomainConventionEjbImpl.class);
	
	private Map<Class<? extends T>,Class<? extends T>> apiToEjbCache = new Hashtable<Class<? extends T>,Class<? extends T>>(10);	
	private Map<Class<? extends T>, Class<? extends T>> ejbToApiCache = new Hashtable<Class<? extends T>, Class<? extends T>>(10);
	private Map<Class<? extends T>, Class<? extends T>> ejbToImplCache = new Hashtable<Class<? extends T>, Class<? extends T>>(10);
	
	private final static Method ejbToImplMethod;
	private final static Method implToEjbMethod;
	
	static { // preloading the methods
		try {
			ejbToImplMethod = EjbModelConventionImpl.class.getMethod("ejbToImpl", Class.class);
			implToEjbMethod = EjbModelConventionImpl.class.getMethod("implToEjb", Class.class);
		} catch (NoSuchMethodException e) {
			throw new NoSuchMethodError(e.getMessage());
		}
	}

	private DomainModelConvention<T> domainModelConvention = null;
	
	public DomainConventionEjbImpl(DomainModelConvention<T> domainModelConvention) {
		this();
		this.domainModelConvention = domainModelConvention;
		
	}

	protected DomainConventionEjbImpl() {
		apiToEjbCache.put((Class<? extends T>)UniqueValue.class, (Class<? extends T>)UniqueValueEjb.class);
		ejbToApiCache.put((Class<? extends T>)UniqueValueEjb.class, (Class<? extends T>)UniqueValue.class);
        
        apiToEjbCache.put((Class<? extends T>)User.class, (Class<? extends T>)UserEjb.class);
        ejbToApiCache.put((Class<? extends T>)UserEjb.class, (Class<? extends T>)User.class);
	}

	/**
	 * Converts an ejb-instance to a domain-model-instance with all relations 
	 * intact. The objects referenced to is partial-instances.
	 * 
	 * @param ejbInstance The ejb-instance to be converted.
	 * @param triggerRegistry 
	 * @return The converted domain-model-instance.
	 */
	@SuppressWarnings("unchecked")
	public <S extends T> S convertToDomainModel(S ejbInstance, int levels, Map<String,S> instanceCache ) {
		return DomainConventionEjbImpl.convertModel(ejbInstance, levels, instanceCache, (EjbModelConventionImpl<S>)this, ejbToImplMethod);
	}

	@SuppressWarnings("unchecked")
	public <S extends T> S convertToEjbModel(S implInstance, int levels, Map<String,S> instanceCache ) {
		return DomainConventionEjbImpl.convertModel(implInstance, levels, instanceCache, (EjbModelConventionImpl<S>)this, implToEjbMethod);
	}
	
	@SuppressWarnings("unchecked")
	public static <S> S convertModel(S instanceToBeConverted, int levels, Map<String,S> instanceCache, EjbModelConventionImpl<S> ejbModelConvention, Method convertTypeMethod ) {
		if (null == instanceToBeConverted || 0 > levels) {
			return null;
		}
		String cacheIdentifier = instanceToBeConverted.getClass().getSimpleName() + ReflectionHelper.invokeMethod(instanceToBeConverted, ejbModelConvention.getIdentifierMethod(instanceToBeConverted)) ; //ejbInstance.getDbId();
		if (instanceCache.containsKey(cacheIdentifier)) {
			return instanceCache.get(cacheIdentifier);
		}
		levels--;
		Class<? extends S> targetClass = (Class<? extends S>) ReflectionHelper.invokeMethod(ejbModelConvention, convertTypeMethod, instanceToBeConverted.getClass()) ; 
		S targetModelObject;
		try {
			targetModelObject = targetClass.newInstance();
		} catch (Exception e) {
			String message = "Could not create DomainModel instance of type: " + targetClass.getSimpleName() + " Source-type was: " + instanceToBeConverted.getClass().getSimpleName();
			log.error(message, e);
			throw new DatabaseException(message, e);
		}
		instanceCache.put(cacheIdentifier, targetModelObject);
		Class[] interfaces = targetClass.getInterfaces();  // Needed: Not all methods are declared in implclass..(!)
		Map<String,Class> dispatchedMethods = new Hashtable<String,Class>();
		for (Class clazz : interfaces) {
			for (Method method : clazz.getMethods()) {
				MethodNamingConvention methodType = MethodNamingConvention.getMethodType(method);
				if( null == methodType) // for greater flexibility regarding business methods (transient)
					continue;
				Method realGetter = methodType.retriveGetterMethod(targetClass, method);
				if( null == realGetter) // for greater flexibility regarding business methods (transient)
					continue;
				Class methodReturnType = realGetter.getReturnType();
				Class existingValue = dispatchedMethods.put(realGetter.getName(), methodReturnType);
				if (null == existingValue || existingValue != methodReturnType) { // only set attributes once..
					Method realSetter = methodType.retriveSetterMethod(targetClass, method);
					if( null == realSetter)
						continue;
					S sourceInstanceValue = ReflectionHelper.<S>invokeMethod(instanceToBeConverted, realGetter);
					Object argument = null;
					if (MethodNamingConvention.isActualLinkToConcept(realGetter)) {
//						triggerRetrieve(triggerRegistry, ejbValue);
						argument = convertModel(sourceInstanceValue, levels, instanceCache, ejbModelConvention, convertTypeMethod);
					} else if (MethodNamingConvention.isActualLinkToConcepts(realGetter)) {
						Collection targetModelCollection = null;
						if (Set.class.isAssignableFrom(methodReturnType)) {
							targetModelCollection = new HashSet();
						} else if (List.class.isAssignableFrom(methodReturnType)) {
							targetModelCollection = new ArrayList();
						} else {
							String message = "Type " + instanceToBeConverted.getClass().getSimpleName() + " has getter which returns collection of unknown type: " + methodReturnType.getSimpleName() + " This collections is ignored in the conversion.";
							log.warn(message);
							continue;
						}
						for (S sourceRelationInstance : (Collection<S>)sourceInstanceValue) {
//							triggerRetrieve(triggerRegistry, ejbRelationInstance);
							Object linkResult = convertModel(sourceRelationInstance, levels, instanceCache, ejbModelConvention, convertTypeMethod);
							if (null != linkResult) {
								targetModelCollection.add(linkResult);
							}
						}
						argument = targetModelCollection;
					} else {
						argument = sourceInstanceValue;
					}
					ReflectionHelper.<S>invokeMethod(targetModelObject, realSetter, argument);
				}
			}
		}
		return targetModelObject;
	}

	/* (non-Javadoc)
	 * @see com.wgo.bpot.wiring.ejb.DomainConventionEjb#apiToEjb(java.lang.Class)
	 */
	public Class<? extends T> apiToEjb(Class<? extends T> apiClass) {
		if (apiToEjbCache.containsKey(apiClass)) {
			return apiToEjbCache.get(apiClass);
		}
		String path = apiClass.getPackage().getName();
		int index = path.lastIndexOf("common");
		if ( 1 >= index ) { throw new RematoException ( "Invalid package name for api-class" + apiClass + " (will not find ejb-class)  ") ; }
		String subpath = path.substring(0,index-1);
		String implClassName = subpath  + ".server.ejb." + apiClass.getSimpleName() + "Ejb";
		Class implClass = null;
		try {
			implClass = Class.forName(implClassName);
		} catch (ClassNotFoundException e) {
			throw new RematoException("Class not found: " + implClassName + ".  domainClass=" + getDomainModelInterface().getName(), e);
		}
		apiToEjbCache.put(apiClass,implClass);
		return implClass;
	}

	/* (non-Javadoc)
	 * @see com.wgo.bpot.wiring.ejb.DomainConventionEjb#ejbToApi(java.lang.Class)
	 */
	public Class<? extends T> ejbToApi(Class<? extends T> ejbClass) {
		if (ejbToApiCache.containsKey(ejbClass)) {
			return ejbToApiCache.get(ejbClass);
		}
		String domainPackageName = ejbClass.getName().substring(0,ejbClass.getName().lastIndexOf(".ejb."));
		String apiClassName = domainPackageName + "." + ejbClass.getSimpleName().substring(0,ejbClass.getSimpleName().indexOf("Ejb"));
		Class apiClass = null;
		try {
			apiClass = Class.forName(apiClassName,true,Thread.currentThread().getContextClassLoader());
		} catch (ClassNotFoundException e) {
			throw new RematoException("Class not found: " + apiClassName, e);
		}
		ejbToApiCache.put(ejbClass,apiClass);
		return apiClass;
	}

	/* (non-Javadoc)
	 * @see com.wgo.bpot.wiring.ejb.DomainConventionEjb#ejbToImpl(java.lang.Class)
	 */
	@SuppressWarnings("unchecked")
	public Class<? extends T> ejbToImpl(Class<? extends T> ejbClass) {
		if (ejbToImplCache.containsKey(ejbClass)) {
			return ejbToImplCache.get(ejbClass);
		}
        String serverEjbStr= ".server.ejb.";
        if (!ejbClass.getName().contains(serverEjbStr)) {
            throw new RematoException("Unable to find corresponding implementation class for: " + ejbClass.getName());
        }
        String domainPackageName = ejbClass.getName().substring(0,ejbClass.getName().lastIndexOf(serverEjbStr));
        String apiClassName = domainPackageName + ".common.impl." + ejbClass.getSimpleName().substring(0,ejbClass.getSimpleName().lastIndexOf("Ejb")) + "Impl";
        Class implClass = null;
        try {
            implClass = Class.forName(apiClassName,true,Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException e) {
            throw new RematoException("Class not found: " + apiClassName, e);
        }
        ejbToImplCache.put(ejbClass, implClass);
        return implClass;
	}

	/* (non-Javadoc)
	 * @see com.wgo.bpot.wiring.common.DomainModelConvention#getRootConceptClass()
	 */
	public Set<Class<? extends T>> getRootConceptClasses() {
		return domainModelConvention.getRootConceptClasses();
	}

	/* (non-Javadoc)
	 * @see com.wgo.bpot.wiring.ejb.EjbModelConventionImpl#implToEjb(java.lang.Class)
	 */
	public Class<? extends T> implToEjb(Class<? extends T> implClass) {
		// TODO: ensure proper transformation, ie. add implementation
		return apiToEjb(implToApi(implClass));
	}

	
}
